{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "490"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "data_folder = os.path.join(os.path.expanduser(\"~\"), \"Data\", \"websites\", \"textonly\")\n",
    "\n",
    "documents = [open(os.path.join(data_folder, filename)).read() for filename in os.listdir(data_folder)]\n",
    "len(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pretty printing has been turned OFF\n"
     ]
    }
   ],
   "source": [
    "pprint([document[:100] for document in documents[:5]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from sklearn.cluster import KMeans\n",
    "from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from sklearn.pipeline import Pipeline\n",
    "\n",
    "n_clusters = 10\n",
    "\n",
    "pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),\n",
    "                     ('clusterer', KMeans(n_clusters=n_clusters))\n",
    "                     ])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Pipeline(steps=[('feature_extraction', TfidfVectorizer(analyzer='word', binary=False, charset=None,\n",
       "        charset_error=None, decode_error='strict',\n",
       "        dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',\n",
       "        lowercase=True, max_df=0.4, max_features=None, min_df=1,\n",
       "        ngram_range=(... n_init=10,\n",
       "    n_jobs=1, precompute_distances=True, random_state=None, tol=0.0001,\n",
       "    verbose=0))])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pipeline.fit(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "labels = pipeline.predict(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cluster 0 contains 2 samples\n",
      "Cluster 1 contains 438 samples\n",
      "Cluster 2 contains 3 samples\n",
      "Cluster 3 contains 12 samples\n",
      "Cluster 4 contains 2 samples\n",
      "Cluster 5 contains 2 samples\n",
      "Cluster 6 contains 1 samples\n",
      "Cluster 7 contains 2 samples\n",
      "Cluster 8 contains 2 samples\n",
      "Cluster 9 contains 26 samples\n"
     ]
    }
   ],
   "source": [
    "from collections import Counter\n",
    "c = Counter(labels)\n",
    "for cluster_number in range(n_clusters):\n",
    "    print(\"Cluster {} contains {} samples\".format(cluster_number, c[cluster_number]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "380.2748466033297"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pipeline.named_steps['clusterer'].inertia_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "inertia_scores = []\n",
    "n_cluster_values = list(range(2, 20))\n",
    "for n_clusters in n_cluster_values:\n",
    "    cur_inertia_scores = []\n",
    "    X = TfidfVectorizer(max_df=0.4).fit_transform(documents)\n",
    "    for i in range(30):\n",
    "        km = KMeans(n_clusters=n_clusters).fit(X)\n",
    "        cur_inertia_scores.append(km.inertia_)\n",
    "    inertia_scores.append(cur_inertia_scores)\n",
    "inertia_scores = np.array(inertia_scores)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABIUAAAJPCAYAAADi72RFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3XlclVXix/Hvc7msbqAiJpSUG+AGpqKOyVVRzH1NRckl\nc0rLfpbZMpNZNulkads4aWm5by1iaa5xMy3Fci00V0YwMjRxBRG4vz+aGC03FHjg3s/7r+d1t/M9\nKvjiyznnMRwOh0MAAAAAAABwKRazAwAAAAAAAKD4UQoBAAAAAAC4IEohAAAAAAAAF0QpBAAAAAAA\n4IIohQAAAAAAAFwQpRAAAAAAAIALuqFSKDc3VxEREerSpYsk6ddff1W7du1Uu3ZttW/fXhkZGfmv\nnThxomrVqqWQkBCtWbOmaFIDAAAAAADgltxQKfTGG28oLCxMhmFIkiZNmqR27dpp3759atu2rSZN\nmiRJSkpK0uLFi5WUlKRVq1ZpxIgRysvLK7r0AAAAAAAAuCnXLYVSU1O1cuVKDRs2TA6HQ5K0fPly\nDRo0SJI0aNAgLVu2TJIUHx+v/v37y93dXcHBwapZs6YSExOLMD4AAAAAAABuxnVLodGjR2vy5Mmy\nWP730mPHjikgIECSFBAQoGPHjkmSfvrpJwUFBeW/LigoSEePHi3szAAAAAAAALhF1yyFPvvsM1Wp\nUkURERH5q4T+yDCM/G1lV3seAAAAAAAAJYv1Wk9+/fXXWr58uVauXKmsrCydPn1acXFxCggI0M8/\n/6yqVasqLS1NVapUkSQFBgYqJSUl//2pqakKDAz80+fWrFlTBw8eLOSpAAAAAAAAuK4aNWrowIED\nN/x6w3G1JUB/8OWXX+rVV1/Vp59+qrFjx6pSpUp66qmnNGnSJGVkZGjSpElKSkpSbGysEhMTdfTo\nUUVHR+vAgQN/Wi1kGMZVVx4BwM0aP368xo8fb3YMAE6G7y0AigLfWwAUhYL2LddcKXSlD5ekp59+\nWvfdd59mzpyp4OBgLVmyRJIUFham++67T2FhYbJarZo2bRrbxwAAAAAAAEqgGy6FoqKiFBUVJUmq\nWLGi1q1bd8XXPfvss3r22WcLJx0AAAAAAACKxHXvPgYApYXNZjM7AgAnxPcWAEWB7y0ASoIbPlOo\nUAflTCEAAAAAAIBCVdC+hZVCAAAAAAAALohSCAAAAAAAwAVRCgEAAAAAALggSiEAAAAAAAAXRCkE\nAAAAAADggiiFAAAAAAAAXBClEAAAAAAAgAuiFAIAAAAAAHBBlEIAAAAAAAAuiFIIAAAAAADABVEK\nAQAAAAAAuCBKIQAAAAAAABdEKQQAAAAAAOCCKIUAAAAAAABcEKUQAAAAAACAC6IUAgAAAAAAcEGU\nQgAAAAAAAC6IUggAAAAAAMAFUQoBAAAAAAC4IEohAAAAAAAAF0QpBAAAAAAA4IIohQAAAAAAAFwQ\npRAAAAAAAIALohQCAAAAAABwQZRCAAAAAAAALohSCAAAAAAAwAVRCgEAAAAAALggSiEAAAAAAAAX\nRCkEAAAAAADggiiFAAAAAAAAXBClEAAAAAAAgAuiFAIAAAAAAHBBlEIAAAAAAAAuiFIIAAAAAADA\nBVEKAQAAAAAAuCBKIQAAAAAAABdEKQQAAAAAAOCCKIUAAAAAAABcEKUQAAAAAACAC6IUAgAAAAAA\ncEGUQgAAAAAAAC6IUggAAAAAAMAFUQoBAAAAAAC4IEohAAAAAAAAF0QpBAAAAAAA4IIohVAg3//y\nvXLycsyOAQAAAAAAbpHV7AAoHezJdtmT7Vr8w2JlZGVoeKPhMgxDtmCbbME2s+MBAAAAAIACYqUQ\nCqRHSA+dyz6nhOQEs6MAAAAAAIBbYDgcDkexD2oYMmFYFJL0c+n6y6y/aFTkKD3S9BGz4wAAAAAA\nABW8b2H7GArMv4y/Vg9crXvev0cBZQLUp24fsyMBAAAAAIACohTCTbnT706tiF2hdnPbqbJPZbW+\ns7XZkQAAAAAAQAFwphBuWsOqDbWkzxL1/bCvdvy8w+w4AAAAAACgACiFcEtswTZN6zRNnRZ00uGT\nh82OAwAAAAAAbhDbx3DLeof11i/nflHMvBhtGrpJ/mX8zY4EAAAAAACug5VCKBQjmoxQ37p91XFB\nR53NPmt2HAAAAAAAcB3ckh6FxuFwaPinw3Xk9BF92v9Tebh5mB0JAAAAAACXUdC+hVIIhSonL0e9\nlvRSOY9ymtNjjiwGi9EAAAAAACgOBe1b+IkdhcpqsWpRr0VKzkjW2LVjzY4DAAAAAACuglIIhc7b\n3VvL+y/X5wc+16tfv2p2HAAAAAAAcAWUQigSFb0ravXA1Xor8S3N3TnX7DgAAAAAAOAPuCU9ikxQ\n+SCtGrBKrWe3ln8Zf3Wo2cHsSAAAAAAA4L9YKYQiFeofqo/7fqy4T+KUeDTR7DgAAAAAAOC/KIVQ\n5Frc3kKzus5St0XdtO/EPrPjAAAAAAAAUQqhmHSp00Uvt3lZMfNilHYmzew4AAAAAAC4PEohFJsh\nEUP0YKMH1WF+B53KOmV2HAAAAAAAXJrhcDgcxT6oYciEYVECOBwOjfp8lHb/slurBq6Sl9XL7EgA\nAAAAADiFgvYtlEIodrl5uYr9OFa5ebla3Hux3CxuZkcCAAAAAKDUK2jfwvYxFDs3i5vmdJ+jk1kn\nNerzURSEAAAAAACYgFIIpvC0euqTvp/o69Sv9dKGl8yOAwAAAACAy7luKZSVlaXIyEiFh4crLCxM\nzzzzjCRp586dat68uRo0aKCuXbvqzJkzkqTk5GR5e3srIiJCERERGjFiRNHOAKVWec/y+nzA5/pg\n5wd697t3zY4DAAAAAIBLuaEzhc6fPy8fHx/l5OSoZcuWevXVVzV69GhNmTJF99xzj95//30dPnxY\nL774opKTk9WlSxft3r376oNyphAuceDXA2r1fiv9u9O/1S2km9lxAAAAAAAolYrkTCEfHx9JUnZ2\ntnJzc+Xn56f9+/frnnvukSRFR0fro48+uom4gFSzYk0t779cD376oDYe2Wh2HAAAAAAAXMINlUJ5\neXkKDw9XQECAWrdurbp166pu3bqKj4+XJC1dulQpKSn5rz98+LAiIiJks9m0cSM/5OP6GldrrPk9\n56vXkl76/pfvzY4DAAAAAIDTu6FSyGKxaMeOHUpNTdWGDRtkt9s1a9YsTZs2TY0bN9bZs2fl4eEh\nSapWrZpSUlK0fft2TZkyRbGxsfnnDQHX0q5GO70e87o6zu+oI6eOmB0HAAAAAACnZi3IiytUqKBO\nnTrp22+/1ZgxY7R69WpJ0r59+7RixQpJkoeHR35B1KhRI9WoUUP79+9Xo0aNLvus8ePH51/bbDbZ\nbLZbmAacRf/6/fXz2Z8VMy9GG4dsVCWfSmZHAgAAAACgRLLb7bLb7Tf9/useNH38+HFZrVb5+voq\nMzNTMTExev7559WgQQP5+/srLy9PgwcPVps2bTR48GAdP35cfn5+cnNz06FDh9SqVSt9//338vX1\n/d+gHDSN6xi7dqy+OvKV1t+/Xj7uPmbHAQAAAACgxCv0g6bT0tLUpk0bhYeHKzIyUl26dFHbtm21\nYMEC1alTR6GhoQoKCtLgwYMlSRs2bFDDhg0VERGhPn36aPr06ZcVQsCNmBQ9SbUr1VbfD/sqJy/H\n7DgAAAAAADidG7olfaEPykoh3ICLuRfVbVE3VS1bVTO7zpRhGGZHAgAAAACgxCqSW9IDZnB3c9fS\nPkv1Q/oP+tsXfzM7DgAAAAAAToVSCCVaGY8yWhG7Qh/v+VhvbXnL7DgAAAAAADiNAt19DDBDZZ/K\nWj1wtVq+31JVylRR33p9zY4EAAAAAECpRymEUqG6b3WtiF2h6DnRquxTWW3vamt2JAAAAAAASjW2\nj6HUaBDQQEv7LFX/j/pre9p2s+MAAAAAAFCqUQqhVIkKjtI7nd9R54WddfDXg2bHAQAAAACg1GL7\nGEqdnqE99cu5XxQzL0abhm5SQNkAsyMBAAAAAFDqsFIIpdJDjR/SgPoD1HFBR525cMbsOAAAAAAA\nlDqGw+FwFPughiEThoWTcTgceuizh3Qo45BWxK6Qh5uH2ZEAAAAAADBNQfsWSiGUarl5ueq9tLe8\nrd6a13OeLAaL3wAAAAAArqmgfQs/QaNUc7O4aUHPBUo9naonVj9B2QgAAAAAwA2iFEKp5+3urfh+\n8Vp3eJ0mfz3Z7DgAAAAAAJQKlEJwCn7eflo1YJWmbZ2m2Ttmmx0HAAAAAIASj1vSw2kElg/UqoGr\nZPvAJv8y/upYq6PZkQAAAAAAKLFYKQSnElI5RMv6LdPgZYO1JXWL2XEAAAAAACixKIXgdJoFNdP7\n3d5Xt0XdtPf4XrPjAAAAAABQIlEKwSl1qt1Jk6InqcO8Djp6+qjZcQAAAAAAKHEoheC0BocP1kON\nH1KH+R2UkZVhdhwAAAAAAEoUw+FwOIp9UMOQCcPCBTkcDo1ePVrb0rZp9cDV8nb3NjsSAAAAAABF\noqB9C6UQnF6eI08DPh6gCzkXtLTPUrlZ3MyOBAAAAABAoSto38L2MTg9i2HRB90+0OkLpzVy5UgK\nSQAAAAAARCkEF+Fp9dTHfT/W1p+26sUvXzQ7DgAAAAAApqMUgsso71leK2NXau6uuZr+7XSz4wAA\nAAAAYCqr2QGA4hRQNkCrB67WPe/foyplqqhHaA+zIwEAAAAAYApWCsHl1KhYQ5/Ffqa/fvZXbfjP\nBrPjAAAAAABgCu4+Bpe17tA6Dfh4gNbFrdOJzBOyJ9slSfZku2zBNkmSLdiWfw0AAAAAQEnGLemB\nAlj0/SI9ufZJbRyyUdV9q0uSjBcMOZ7n3ycAAAAAoHQpaN/CmUJwaf3q9dOxs8cUMy9GG4duVGWf\nymZHAgAAAACgWHCmEFzeY80eU4+QHuq8oLPOZZ8zOw4AAAAAAMWC7WOAJIfDoaHLh+rY2WP6/MDn\nbB8DAAAAAJQ6Be1bWCkE6LcvnBmdZ8jL6iVJyryYaXIiAAAAAACKFqUQ8F/ubu5a3HuxJKnzQraS\nAQAAAACcG6UQcAl3N3dJUvUK1RUzL0anL5w2OREAAAAAAEWDUgi4gve6vqeGAQ0VPSdav2b+anYc\nAAAAAAAKHaUQcAUWw6K3O76tVtVbqfXs1vrl3C9mRwIAAAAAoFBx9zFAkj3ZLnuyPf/aFmyTJEVV\nj1JCcoI+TPpQ6+5fp2rlqpkXEgAAAACAayho30IpBNyAiV9N1MztM7X+/vWq7lvd7DgAAAAAAPxJ\nQfsWaxFmAZzGM/c8Ix93H0V9EKX1969XjYo1zI4EAAAAAMAtoRQCbtBjzR6Tt7u3bLNtWjNwjUL9\nQ82OBAAAAADATaMUAgpg+N3D5W31Vps5bbRqwCo1rNrQ7EgAAAAAANwUSiGggOIaxsnL6qX289rr\ns/6fqUlgE7MjAQAAAABQYJRCwE3oU7ePvKxe6rSgkz7u+7Fa3tHS7EgAAAAAABSIxewAQGnVpU4X\nze85Xz0X99QXh78wOw4AAAAAAAVCKQTcgnY12mlpn6Xq92E/rdy/0uw4AAAAAADcMEoh4BZFBUdp\nef/lGhI/RJ/s+cTsOAAAAAAA3BDOFAIKQbOgZvp8wOfqtKCTsnKy1L9+f7MjAQAAAABwTZRCQCFp\ndFsjrY1bq5h5McrMydTQiKFmRwIAAAAA4KoohYBCVK9KPSUMSlC7ue2UeTFTI5uONDsSAAAAAABX\nRCkEFLLalWrLPsiutnPaKjMnU2NajDE7EgAAAAAAf2I4HA5HsQ9qGDJhWKBYpZ5OVds5bTWg/gA9\n1+o5GYZhdiQAAAAAgBMraN9CKQQUoWNnjyl6brQ61+qsl9u+TDEEAAAAACgylEJACXP8/HHFzItR\ny9tb6vUOr1MMAQAAAACKBKUQUAJlZGXo3vn3qn6V+nqn8zuyGBazIwEAAAAAnExB+xZ+MgWKga+X\nr9YMXKN9J/Zp8LLBysnLMTsSAAAAAMDFsVIIKEbnL55Xj8U9VN6zvOb3nC8PNw+zIwEAAAAAnAQr\nhYASzMfdR8v7LVd2brZ6L+mtrJwssyMBAAAAAFwUpRBQzDytnvqwz4fydvdW14Vddf7iebMjAQAA\nAABcEKUQYAJ3N3ct6LlA1cpVU4d5HXTmwhmzIwEAAAAAXAylEGASN4ubZnWbpbr+ddVubjudzDxp\ndiQAAAAAgAuhFAJMZDEsmtZpmpoHNVebOW2Ufi7d7EgAAAAAABdBKQSYzDAMTYmZoo41O8o226a0\nM2lmRwIAAAAAuABKIaAEMAxD/2j7D8XWi1XUB1FKOZVidiQAAAAAgJOzmh0AwP/8rdXf5O3uragP\norTu/nW6y+8usyMBAAAAAJwUpRBQwjze/HF5W71l+8CmtXFrVadyHbMjAQAAAACcEKUQUAI93ORh\nebt7q82cNlo1YJXqB9Q3OxIAAAAAwMlQCgEl1ODwwfK2eqvd3HZaEbtCd1e72+xIAAAAAAAnQikE\nlGB96/WVl9VL986/V8v6LVOL21sU+DPsyXbZk+3517ZgmyTJFmzLvwYAAAAAuB7D4XA4in1Qw5AJ\nwwKl1qoDq3T/J/drSZ8lt1TkGC8YcjzP1x4AAAAAOKOC9i3ckh4oBTrU7KDFvRfrvqX3afWB1WbH\nAQAAAAA4AUohoJRofWdrLeu3THGfxCl+b7zZcQAAAAAApRxnCgGlSIvbW+jzAZ+r04JOysrJUt96\nfc2OBAAAAAAopa65UigrK0uRkZEKDw9XWFiYnnnmGUnSzp071bx5czVo0EBdu3bVmTNn8t8zceJE\n1apVSyEhIVqzZk3Rpgdc0N3V7taauDUavXq0Zu+YbXYcAAAAAEApdc1SyMvLSwkJCdqxY4d27dql\nhIQEbdy4UcOGDdMrr7yiXbt2qUePHpo8ebIkKSkpSYsXL1ZSUpJWrVqlESNGKC8vr1gmAriSBgEN\n9MWgL/T3hL/rnW/fMTsOAAAAAKAUuu6ZQj4+PpKk7Oxs5ebmys/PT/v379c999wjSYqOjtZHH30k\nSYqPj1f//v3l7u6u4OBg1axZU4mJiUUYH3BdIZVDZB9k1z83/VOvb37d7DgAAAAAgFLmuqVQXl6e\nwsPDFRAQoNatW6tu3bqqW7eu4uN/O+h26dKlSklJkST99NNPCgoKyn9vUFCQjh49WkTRAdSoWEMb\nBm/QtK3T9PJXL5sdBwAAAABQilz3oGmLxaIdO3bo1KlTiomJkd1u16xZszRq1ChNmDBBXbt2lYeH\nx1XfbxjGFR8fP358/rXNZpPNZitweADS7RVu15eDv1T03Gidv3heE1pPuOrXHQAAAADAedjtdtnt\n9pt+/w3ffaxChQrq1KmTvv32W40ZM0arV6+WJO3bt08rVqyQJAUGBuavGpKk1NRUBQYGXvHzLi2F\nANya28rdJvsgu9rPa6/zF8/rtfavUQwBAAAAgJP74yKbF154oUDvv+b2sePHjysjI0OSlJmZqbVr\n1yoiIkLp6emSftta9tJLL+nhhx+WJHXt2lWLFi1Sdna2Dh8+rP3796tp06YFCgTg5viX8dcX93+h\nTSmbNGLFCOU5OOQdAAAAAHB11yyF0tLS1KZNG4WHhysyMlJdunRR27ZttWDBAtWpU0ehoaEKCgrS\n4MGDJUlhYWG67777FBYWpnvvvVfTpk1jtQJQjPy8/bQ2bq1+SP9BQ+OHKjcv1+xIAAAAAIASynA4\nHI5iH9QwZMKwgMs4l31O3Rd3VyXvSprbY67c3dwlScYLhhzP87UHAAAAAM6ooH3Lde8+BqD0KeNR\nRp/2/1Rns8+qz9I+upBzwexIAAAAAIAShlIIcFJeVi993PdjWS1WdVvUTecvnjc7EgAAAACgBKEU\nApyYh5uHFvVeJP8y/uq0oJPZcQAAAAAAJQilEODkrBarZnefrVoVa0mStqRuMTkRAAAAAKAk4KBp\nwEU4HA5ZXrSoWrlqalW9lSa2nahg32CzYwEAAAAACgkHTQO4IsMwJEk/PvKjQiqF6O4Zd2vs2rHK\nyMowORkAAAAAwAyUQoCLKetRVs/bntfuh3frxPkTqvN2Hb2d+LYu5l40OxoAAAAAoBixfQxwIcYL\nhhzPX/61t+vYLo1ZM0b/OfUfvRL9irrW6Zq/qqg0sCfbZU+251/bgm2SJFuwLf8aAAAAAFxBQfsW\nSiHAhVypFJJ+O29o1YFVGrN2jPx9/PVq+1fVuFpjExLemqvNDwAAAABcAWcKASgwwzB0b617tfOh\nnYqtH6suC7so7pM4pZxKMTsaAAAAAKCIUAoByGe1WDX87uHa98g+BVcIVvj0cD27/lmdvnDa7GgA\nAAAAgEJGKQTgT8p5ltOENhO086GdOnrmqOq8XUfvfPuOcvJyzI4GAAAAACgklEIAriqofJBmd5+t\nFbErtOSHJWrw7wZasW8FZ4IBAAAAgBOgFAJwXY1ua6T196/XK+1e0Zi1Y9Rubjvt+HmH2bEAAAAA\nALeAUgjADTEMQ51rd9auh3apZ2hPdZjXQUPih+jo6aNmRwMAAAAA3ARKIQAF4u7mrhFNRujHR35U\n1TJV1eCdBhqXME5ns8+aHQ0AAAAAUACUQgBuSgWvCpoYPVHbhm/ToZOHVPut2nr3u3eVm5drdjQA\nAAAAwA2gFAJwS6r7Vte8nvMU3y9ec3fNVfj0cK0+sNrsWAAAAACA66AUAlAomgQ20ZeDv9SE1hP0\n6OePqsO8Dtp9bLfZsQAAAAAAV0EpBKDQGIah7iHd9f2I79WxVke1ndNWDy5/UGln0syOBgAAAAD4\nA0ohAIXOw81DoyJHad+j++Tr5at6/66nF798Ueeyz5kdDQAAAADwX5RCAIqMr5evJrefrG8f/FZJ\n6Umq83Ydvb/9fQ6jBgAAAIASwHA4HI5iH9QwZMKwgEuyJ9tlT7bnX9uCbZIkW7At/7q4bE7drCfW\nPKFz2ef0avtXFX1XdKF+vvGCIcfzfG8BAAAA4JoK2rdQCgEoVg6HQx/t+UhPrXtKIZVDNLndZIX5\nhxXKZ1MKAQAAAHBlBe1b2D4GoFgZhqHeYb2VNCJJ0XdGy/aBTQ9/9rCOnT1mdjQAAAAAcCmUQgBM\n4Wn11Ojmo7X3kb3ysnqp7rS6evmrl5V5MdPsaAAAAADgEiiFAJiqondFTe0wVZuHbda2tG2q83Yd\nzd05V3mOPLOjAQAAAIBT40whACXKxiMb9cSaJ5STl6PX2r9WoMOwOVMIAAAAgCvjTCEApVrLO1pq\n8wOb9WSLJzUkfoi6LeqmH4//aHYsAAAAAHA6lEIAShzDMNSvXj/tGblHLW9vqb/M+oseWfmI0s+l\nmx0NAAAAAJwGpRCAEsvL6qUn//Kk9j6yV4YMhf4rVP/c+E9l5WSZHQ0AAAAASj1KIQAlXmWfynqr\n41vaNHSTvkn9RiFvh2jh7oWcTQYAAAAAt4CDpgGUOl8mf6kn1jwhN4ubXmv/mlre0VISB00DAAAA\ncG0cNA3A6UUFRynxwUQ92vRRxX4Uq15LeunArwfMjgUAAAAApQqlEIBSyWJYNLDBQP34yI9qfFtj\nNXuvmSQpOzfb5GQAAAAAUDqwfQyAUzh29piqvlZVjas11sJeC1WzYk2zIwEAAABAsWL7GACXFFA2\nQJJ0f4P71Xxmc83fNd/kRAAAAABQsrFSCIDT+P2g6R0/71C/D/spMihS/+r4L5X1KGt2NAAAAAAo\ncqwUAuDywquG67vh38lqWNVoeiNtS9tmdiQAAAAAKHEohQA4pTIeZTSz20y9YHtBMfNi9Prm11mh\nCAAAAACXoBQC4NT61++vLcO2aOH3C9V5YWeln0s3OxIAAAAAlAiUQgCc3l1+d2njkI2qX6W+IqZH\n6IvDX5gdCQAAAABMRykEwCW4u7lrUvQkvd/tfcV9Eqdn1z+ri7kXzY4FAAAAAKbh7mMAnMbvdx+7\nnl/O/aJBywYpIytDC3stVLBvcNGHu0n2ZLvsyfb8a1uwTZJkC7blXwMAAACAVPC+hVIIgNO40VJI\nkvIceXp98+uatHGS/tXxX+pTt08Rp7t1BZkfAAAAANfDLekB4AZYDIseb/64Vg5YqWe/eFYPLn9Q\n5y+eNzsWAAAAABQbSiEALq1xtcbaNnybsnKz1HhGY+06tsvsSAAAAABQLCiFALi8cp7lNLfHXD3T\n8hm1ndNW/0r8F1tcAQAAADg9SiEA+K+4hnH6eujXmrVjlnou6alfM381OxIAAAAAFBlKIQC4RK1K\ntfT10K91l+9dCn8nXBv+s8HsSAAAAABQJCiFAOAPPK2eei3mNb3T+R31/bCvxtvHKycvx+xYAAAA\nAFCoKIUA4Co61uqobcO3aeORjWozu41STqWYHQkAAAAACg2lEABcw23lbtOauDXqWKujGr/bWMv2\nLjM7EgAAAAAUCkohALgOi2HR0y2fVny/eD2++nGNXDFSmRczzY4FAAAAALeEUggAblCzoGba9tdt\nOp55XJHvRSopPcnsSAAAAABw0yiFAKAAfL18tajXIj0W+ZiiPojSu9+9K4fDYXYsAAAAACgwSiEA\nKCDDMPRAowe0YfAGvb31bfX9sK8ysjLMjgUAAAAABUIpBAA3KdQ/VFuGbVFAmQBFTI/QNynfmB0J\nAAAAAG4YpRAA3AIvq5fe6viWXo95Xd0Xd9fLX72s3Lxcs2MBAAAAwHVRCgFAIegW0k3fDf9Oqw+u\nVvt57fXTmZ/MjgQAAAAA10QpBACFJKh8kL64/wtFVY9So+mN9Nm+z8yOBAAAAABXRSkEAIXIzeKm\ncVHjtLTPUo1cOVL/t+r/dCHngtmxAAAAAOBPKIUAoAjcU/0ebf/rdh05dUTNZzbXvhP7zI4EAAAA\nAJcxHA6Ho9gHNQyZMCwAJ2RPtsuebM+/tgXbJEm2YFv+tZkcDofe+fYdjbOP0+R2kzWo4SAZhnFT\nn2W8YMjxPN87AQAAAFxZQfsWSiEAKAa7j+1Wv4/6KbxquP7d6d8q71m+wJ9BKQQAAADgWgrat7B9\nDACKQf2A+tr64FaVdS+riOkR2np0q9mRAAAAALg4SiEAKCY+7j6a3mW6JrWdpE4LOmnypsnKc+SZ\nHQsAAAAImGZUAAAgAElEQVSAi6IUAoBi1qduH219cKuW/bhMHed31LGzx8yOBAAAAMAFUQoBgAmq\n+1bXl4O/VJNqTRQxPUJrDq4xOxIAAAAAF0MpBAAmsVqsmtBmgub3nK+h8UM1du1YZedmmx0LAAAA\ngIu4bimUlZWlyMhIhYeHKywsTM8884wkKTExUU2bNlVERISaNGmirVt/OzQ1OTlZ3t7eioiIUERE\nhEaMGFG0MwCAUq71na2146Ed2nN8j1rOaqmDvx40OxIAAAAAF2C93gu8vLyUkJAgHx8f5eTkqGXL\nltq4caOee+45TZgwQTExMfr88881duxYJSQkSJJq1qyp7du3F3l4AHAWlX0qa3m/5Xor8S01m9lM\nb3R4Q7H1Y82OBQAAAMCJ3dD2MR8fH0lSdna2cnNz5efnp6pVq+rUqVOSpIyMDAUGBhZdSgBwAYZh\naFTkKK0ZuEYvfPmChsQP0dnss2bHAgAAAOCkbqgUysvLU3h4uAICAtS6dWvVrVtXkyZN0hNPPKE7\n7rhDTz75pCZOnJj/+sOHDysiIkI2m00bN24ssvAA4IwibovQd8O/kyFDd8+4W9vTWHkJAAAAoPDd\nUClksVi0Y8cOpaamasOGDbLb7XrggQf05ptv6siRI5o6daqGDh0qSapWrZpSUlK0fft2TZkyRbGx\nsTpz5kyRTgIAnE1Zj7Ka1W2WxkeNV/t57fXG5jfMjgQAAADAyVz3TKFLVahQQZ06ddK3336rxMRE\nrVu3TpLUu3dvDRs2TJLk4eEhDw8PSVKjRo1Uo0YN7d+/X40aNbrss8aPH59/bbPZZLPZbmEaAOCc\n+tfvr8igSPX7sJ8k6cyFMyrnWc7kVAAAAABKArvdLrvdftPvNxwOh+NaLzh+/LisVqt8fX2VmZmp\nmJgYjRs3TmPHjtXUqVMVFRWl9evX6+mnn9bWrVt1/Phx+fn5yc3NTYcOHVKrVq30/fffy9fX93+D\nGoauMywA4BIXci7I6x9equtfV/H94lWjYg2zIwEAAAAoYQrat1x3pVBaWpoGDRqkvLw85eXlKS4u\nTtHR0ZoxY4ZGjhypCxcuyNvbWzNmzJAkbdiwQePGjZO7u7ssFoumT59+WSEEACg4T6unJGlEkxFq\nMauF5vWYp3Y12pmcCgAAAEBpdt2VQkUyKCuFAKDAjBcMOZ536MvkL9Xvo356ssWTGt1stAzDMDsa\nrsKebJc92Z5/bQu2SZJswbb8awAAAKCwFLRvoRQCgFLi91JIkv6T8R91X9xd9arU04zOM+Tt7m1y\nOlzPpX9/AAAAQFEoaN9yQ3cfAwCULNV9q2vT0E3KyctRqw9aKfV0qtmRAAAAAJQylEIAUEr5uPto\nQc8F6hPWR5HvRWrTkU1mRwIAAABQihTolvQAgJLFMAyN/ctY1a9SXz0W99BLbV7S8LuHmx0LLoDz\nkgAAAEo/zhQCgFLiemfS7DuxT90WdZOtuk1v3PuGPNw8ijEdrseZzxRy5rkBAACUJpwpBAAuqnal\n2toybIuOnjmq6DnR+uXcL2ZHAgAAAFCCUQoBgBMp71ley/otU1T1KDV5t4m2pW0zOxIAAACAEopS\nCACcjMWwaEKbCZrSfopi5sVo4e6FZkcCAAAAUAJx0DQAOKleYb1Uu1JtdV/cXdt/3q6JbSfKzeJm\ndiwAAAAAJQQrhQDAidUPqK/EYYn6Lu07dVrQSSczT5odCQAAAEAJQSkEAE6ukk8lrR64WqGVQ9X0\nvaZKSk8yOxIAAACAEoBSCABcgNVi1dQOU/Vcq+dk+8Cm+L3xZkcCAAAAYDLOFAIAF3J/w/sVUjlE\nvZb00s5jO/X3Vn+XxeD3AwAAAIAr4icBAHAxTQObKnFYolYdWKU+S/vozIUzZkcCAAAAYAJKIQBw\nQbeVu00JgxLk5+Wn5jOb6+CvB82OBAAAAKCYUQoBgIvytHrq3S7vakSTEWoxq4XWHVpndiQAAAAA\nxYhSCABcmGEYGtFkhJb0XqK4T+I09ZupcjgcZscCAAAAUAwohQAAigqO0uYHNmvOrjkatGyQMi9m\nmh0JAAAAQBHj7mMAAElSdd/q2jR0k4bGD1WrD1rpk76fKKh8kNmxAFPYk+2yJ9vzr23BNkmSLdiW\nfw0AAFDaUQoBAPL5uPtoYa+FemXTK4p8L1JLei/RX+74i9mxgGJ3afljvGDIPthuah4AAICiwPYx\nAMBlDMPQUy2f0ntd3lOPxT307nfvmh0JAAAAQBFgpRAA4IrurXWvNg7dqG6Lumn7z9v1eofX5eHm\nUahjsEUHAAAAMA+lEADgqmpXqq0tw7Zo4McD1W5uOy3ts1RVylQptM9niw4AAABgHraPAQCuqbxn\neS3rt0yt7milpu821ba0bWZHAgAAAFAIKIUAANdlMSya0GaCXm3/qmLmxWjh7oVmRwIAAABwi9g+\nBgC4Yb3DeqtOpTrqvri7tv+8XRPbTpSbxc3sWAAAAABuAiuFAAAFUj+gvhKHJeq7tO/UeWFnncw8\naXYkAAAAADeBUggAUGCVfCpp9cDVCqkUosj3IpWUnmR2JAAAAAAFRCkEALgpVotVUztM1d/u+Zts\nH9gUvzfe7EgAAAAACoAzhQCgBLMn22VPtkuSoqpHabx9vKTLb+VutkHhgxTqH6peS3pp57Gd+nur\nv8ti8DsHAAAAoKSjFAKAEqwklT/X0jSwqRKHJeYXQ7O7z1ZZj7JmxwIAAABwDfwqFwBQKG4rd5sS\nBiXIz8tPzWc216GTh8yOBAAAAOAaKIUAAIXG0+qpd7u8q4fufkgtZrbQukPrzI4EAAAA4CoohQAA\nhcowDI1sOlKLey9W3CdxmvrNVDkcDrNjAQAAAPgDSiEAQJGICo7S5gc2a86uORq0bJAyL2aaHQkA\nAADAJSiFAABFprpvdW0auknZudlq9UErpZ5ONTsSAAAAgP/i7mMAgCLl4+6jhb0W6pVNryjyvUgt\n7bNULW5vYXYswKXZk+2yJ9vzr3+/y2FpueMhAAAoHJRCAIAiZxiGnmr5lBoENFCPxT30UuuX9ODd\nD5odC3BZl5Y/xguG7IPtpuYBAADmYPsYAKDY3FvrXn015CtN2TxFI1eMVHZuttmRAAAAAJdFKQQA\nKFa1K9XWlmFblHI6Re3mttMv534xOxIAAADgkiiFAADFrrxneS3rt0yt7milpu821ba0bWZHAgAA\nAFwOZwoBAExhMSya0GaCGlZtqJh5MZKkDvM6yNfLVxU8K6iCV4UrXvt6+aqCVwVV8Kygcp7lZDH4\n/QYAAABwMyiFAACm6h3WWy3vaKnbXrtNj0U+poysDJ26cEqnsk4pIytDqadTderCb9e/P/b78+cu\nnlM5j3JXLZD+WCJd6drb6i3DMMz+YwAAAACKHaUQAMB0VctWlfTbQdQFkZuXq9MXTl9WGv3x+ti5\nY9p3Yt//Hr+kcDp14ZRy8nKuuArpeiuWLr12d3Mvij8WAAAAoEhRCgEASi03i5v8vP3k5+13059x\nIefCn4qiP14fPnn4t+urlE8ebh7XXJHk5/VbvqT0JNWsWFMebh6F9UcAAAAA3DRKIQCAS/O0eqqK\ntYqqlKlyU+93OBw6f/H8VQuljKwMncg8IUnqvqi7jpw6ojv97lSYf5jCKocp1D9UYf5hqlOpjrzd\nvQtzagAAAMA1UQoBAHALDMNQGY8yKuNRRoEKvOrrJn89Wfse3aesnCztP7FfSelJSkpP0rK9y/Ty\nVy/rwK8HFFQ+6LeSqHKYwvx/K4xCK4eqnGe5YpwRAAAAXAWlEAAAxcjL6qX6AfVVP6D+ZY9fzL2o\ngycPak/6HiWlJ2nNoTV6Y8sb2nt8ryr7VL6sLPq9MKroXdGkWQAAAMAZUAoBAFACuLu5K6RyiEIq\nh6hHaI/8x3PzcvWfU//JL4u+Tv1a721/T3vS98jH3ee3gqhyaH5ZFOYfpiplqnBHNQAAAFwXpRAA\nACWYm8VNd/ndpbv87lKn2p3yH3c4HDp65qiS0pO0J32Pdh3bpUU/LFJSepIcDsf/VhRdUhgFlQ+i\nLAIAAEA+SiEAAEohwzAUVD5IQeWD1L5G+/zHHQ6H0s+n559ZtCd9jz7b/5mS0pN0NvvsZSXR79fB\nvsFys7iZOBsAAACYgVIIAAAnYhiGqpT57W5qtmDbZc+dzDypPcf35JdFCckJSkpPUvq5dNWuVPtP\nZVHNijXl7uZuzkQAAABQ5CiFAABwEX7efmpxewu1uL3FZY+fzT6rvcf35q8umrNrjpLSk5RyKkU1\nKtb40+qiOpXryMvqZdIsAAAAUFgohQAAcHFlPcqqcbXGalyt8WWPZ17M1L4T+/JXF32Y9KH2HN+j\ng78e1O0Vbs8viyQp8Wii6lSqowpeFcyYAgAAAG4CpRAAALgib3dvNazaUA2rNrzs8Yu5F3Xg1wP5\nZZEkPfTZQ/rxxI/y9fL97S5qlUIU6h+af0e1wHKBHHINAABQwlAKAQCAAnF3c1eof6hC/UPVM7Sn\nnkt4Ttv+uk15jjylnk7VnvQ92nt8r3745Qd9mPSh9h7fq3MXz+UXRJcWRjUr1pSHm4fZUwIAAHBJ\nlEIAAKBQWAyL7qhwh+6ocIdiasZc9tzJzJP68cSP+YXR7J2ztSd9j46cOqLqvtWvuLrI18vXpJkA\nAAC4BkohAABQ5Py8/dQsqJmaBTW77PELORd08OTB/LJo/eH1ejvxbf144keV9SibXxaFVP5fYRRU\nPkgWw2LSTAAAAJwHpRAAADCNp9Uz/85ml3I4HDp65qj2Ht+bXxgt37dce9L36PSF06pTuc6fVhfV\nqlhLnlZPk2YCAABQ+lAKAQCAEscwDAWVD1JQ+SBF3xV92XOnsk5dthVt/u752pO+R8kZybq9wu1X\nXF1U0buiSTMBAAAouSiFAABAqVLBq4KaBjZV08Cmlz2enZutQycP5a8u2nBkg2Zsm6E96XvkZfX6\nrSD6Q1l0R4U72IoGAABcFqUQAABwCh5uHvmHVHcP6Z7/uMPhUNrZNO09vje/MFp5YKX2Ht+rE+dP\nqHal2n8qjGpVrCVvd28TZwMAAFD0KIUAAKaxJ9tlT7ZLkqKqR2m8fbwkyRZsky3YZlouOBfDMFSt\nXDVVK1dNbe5sc9lzZy6c0Y8nfswvi5YkLdHe43t18NeDqlaumkL9QyVJ+0/sV61KtcyIDwAAUGQo\nhQAApqH8gdnKeZZT42qN1bha48sez8nLyd+KtnL/SrV8v6Xu8rtLcQ3idF/d+1TZp7JJiQEAAAoP\nm+gBAAD+wGqxqnal2upap6skKXV0qp5r9Zy+OvKVarxZQ90WddOHSR8qKyfL5KQAAAA3j1IIAADg\nOtzd3NWxVkct7LVQKaNT1L1Od03bOk2BUwI1/NPh+uo/XynPkWd2TAAAgAJh+xgAAEABlPcsryER\nQzQkYohSTqVowe4FenjFwzp38ZwG1B+guAZxqlO5jtkxXdql55XZk+3521TZsgoAwOUMh8PhKPZB\nDUMmDAsAgGmMFww5nnfO//uceW7Sjc3P4XBox887NHfXXC38fqFuL3+74hrEqV+9fvIv419MSW8O\nf38AADiPgvYtbB8DAAC4RYZhKOK2CE2JmaKU0Sl6sfWL2nx0s2q9VUtdFnbRkh+WKPNiptkxAQAA\nLkMpBAAAUIisFqs61Oyg+T3nK2V0ivqE9dG7295V4JRAPRD/gOzJds4fAgAAJQKlEAAAQBEp51lO\n9ze8X2vj1mr3w7sVUjlEoz4fpTvfuFPPrn9WSelJZkcEAAAu7JqlUFZWliIjIxUeHq6wsDA988wz\nkqTExEQ1bdpUERERatKkibZu3Zr/nokTJ6pWrVoKCQnRmjVrijY9AABAKRFYPlBP/uVJ7Xp4lz7t\n/6ku5l5U9Jxo3T3jbr2++XUdO3vM7IgAAMDFXLMU8vLyUkJCgnbs2KFdu3YpISFBGzdu1FNPPaUJ\nEyZo+/btevHFFzV27FhJUlJSkhYvXqykpCStWrVKI0aMUF4ey6MBAAAu1SCggSa3n6yU0Sma1HaS\ntqVtU52366jj/I5auHuhzl88b3ZEAADgAq67fczHx0eSlJ2drdzcXPn5+alq1ao6deqUJCkjI0OB\ngYGSpPj4ePXv31/u7u4KDg5WzZo1lZiYWITxAQAASi83i5va1WinOT3m6OjjRxVbP1azd85W4JRA\nDYkfovWH1is3L9fsmAAAwElZr/eCvLw8NWrUSAcPHtTDDz+sunXratKkSWrZsqXGjBmjvLw8ffPN\nN5Kkn376Sc2aNct/b1BQkI4ePVp06QEAAJxEGY8yGthgoAY2GKi0M2la+P1CjVk7Runn0jWg/gDF\nNYxTvSr1zI4JAACcyHVXClksFu3YsUOpqanasGGD7Ha7HnjgAb355ps6cuSIpk6dqqFDh171/YZh\nFGpgAAAAZ3dbudv0ePPHtf2v27Vq4CpJUod5HRQxPUJTvpmitDNpJicEAADO4LorhX5XoUIFderU\nSd9++60SExO1bt06SVLv3r01bNgwSVJgYKBSUlLy35Oampq/teyPxo8fn39ts9lks9luIj4AAIBz\nq1elnv7Z7p96ue3LsifbNW/3PE2YNkFNA5sqrkGceoT0UBmPMmbHBAAAJrDb7bLb7Tf9fsPhcDiu\n9uTx48dltVrl6+urzMxMxcTEaNy4cRo7dqymTp2qqKgorV+/Xk8//bS2bt2qpKQkxcbGKjExUUeP\nHlV0dLQOHDjwp9VChmHoGsMCAOB0jBcMOZ53zv/7nHluUsmc3/mL5xW/N15zd83V1ylfq2udropr\nEKc2d7aRm8WtQJ9VEudXmJx9fgAAXKqgfcs1VwqlpaVp0KBBysvLU15enuLi4hQdHa0ZM2Zo5MiR\nunDhgry9vTVjxgxJUlhYmO677z6FhYXJarVq2rRpbB8DAAAoZD7uPupfv7/61++vY2ePaeH3C/XM\n+mf005mfFFs/VnEN4tSwakOzYwIAgBLumiuFimxQVgoBAFyMM69WcOa5SaVrfknpSZq3a57m7Zon\nXy9fxTWIU2z9WAWWv/J2fql0ze9mOPv8AAC4VEH7luseNA0AAIDSIcw/TC+3fVnJ/5esN+99U3uP\n71W9f9dTu7ntNGfnHJ25cMbsiAAAoAShFAIAAHAyFsMiW7BNM7vN1E+P/6RhEcO0NGmpbp96uwZ8\nPECrDqxSTl6O2TEBAIDJbvjuYwAAACh9vN291bdeX/Wt11fp59K16PtFGpcwTkPih6h/vf5mxwMA\nACZipRAAAICL8C/jr0cjH1Xig4lKGJQgL6uXJGnEihH6+ezPJqcDAADFjVIIAADABYVUDtHLbV+W\nJHlbvVV3Wl2NSxin0xdOm5wMAAAUF0ohAAAAF/dazGvaNnybkjOSVeutWnpzy5vKzs02OxYAAChi\nlEIAAABQdd/qmtNjjtYMXKNVB1Yp5O0QLdy9UHmOPLOjAQCAIkIpBAAAgHwNqzbUygErNbPrTE3d\nPFWNZzTW2oNrzY4FAACKAKUQAAAA/qT1na21ZdgWPXvPsxq5cqTazW2n7376zuxYAACgEFEKAQAA\n4IoMw1DvsN76YcQP6hnSU50Xdlb/j/rr4K8HzY4GAAAKAaUQAAAArsndzV0PN3lY+x/dr7DKYWr6\nXlON+nyUfjn3i9nRAADALbCaHQAAAGdlT7bLnmyXJEVVj9J4+3hJki3YJluwzbRcwM0q61FWz0U9\np782/qv+seEfCv1XqB6LfEyPN39cZT3Kmh0PAAAUEKUQAABFhPIHzqpKmSp64943NCpylJ5LeE61\n3qql51o9pwcbPSh3N3ez4wEAgBvE9jEAAADclBoVa2hBrwVaEbtCy/YuU9i0MC39YakcDofZ0QAA\nwA2gFAIAAMAtaXRbI62JW6NpHadp4saJ+v/27jyuqjLx4/j3XBYBRUETFDFBkBRcQE1z5SKKYq45\nSWZkauY+mdWUTbm0jDaO+tOstNWaKcNqzA1NLW5oZaTikhuIkktqMqnlFgL390czd3JaFAUO997P\n+/Xq1YG7nO/zQm+Hb8/znLavtFXGwQyzYwEAgCugFAIAAECZ6BbRTZvv26wHbnlAw5cPV/Jbydp+\nfLvZsQAAwG+gFAIAAECZsRgWDWo2SHvH7VXPyJ7q/o/uSl2aqvzT+WZHAwAA/4NSCAAAAGXO28Nb\n49uOV874HDUMaKhWL7XSxA8nquB8gdnRAADAv1EKAQAAoNxUr1Jd0xKmadeYXbpYdFGN5zfWXzb8\nRecKz5kdDQAAt8ct6QEAAFDu6lSroxdufUETbpmgxz9+XFHzozQlfoqGxQ2Tp4VL0tKw5dtky7c5\njq1hVkmSNczqOAYA4GrwX2AAAABUmKhaUVpy+xJlHc3SI+sf0ezPZ+sviX9R/8b9ZRiG2fGcws/L\nH2OaIds9NlPzAACcF8vHAAAAUOHa1Gujj+/+WHO6z9FU21S1f629Nny9wexYAAC4FUohAAAAmMIw\nDCU3Slb2yGyNaT1GqUtT1Xtxb3317VdmRwMAwC1QCgEAAMBUHhYPpbZI1d5xe9UlrIu6vNFFQ5cN\n1aEzh8yOBgCAS6MUAgAAQKXg4+mjB9o9oNzxuQqpFqK4hXH607o/6bsL35kdDQAAl0QpBAAAgEql\nhk8NPZP4jHaO3qnTF0/rpvk36a+f/lUXLl0wOxoAAC6Fu48BAIBS+/ktseMbxGuqbaokbomNshXi\nH6KXer+kie0m6rGPHtNzWc9pmnWahrQYIg+Lh9nxAABweobdbrdX+EkNQyacFgAA4Kr8vPSy5dsc\nRZcrll7GNEP2Kc5xXfb54c/1p/U/LSebnjhdvaN6X/E29s40vmvhauNzp797AFAeStu3UAoBAAC4\nMWcrFex2u1blrtKj6x9VoG+gnu36rNrXb/+bz3e28ZWWK4/PlccGAOWltH0LewoBAADAaRiGoV5R\nvbR91HYNix2mlPdS1D+tv/ac3GN2NAAAnA6lEAAAAJyOh8VDQ+OGKmdcjtqHtlfnRZ01YvkIHf3+\nqNnRAABwGpRCAAAAcFq+Xr56uMPDyhmXo5q+NdXsxWaatH6STl88bXY0AAAqPUohAAAAOL1A30A9\n2+1ZbR+1XSfOnVDUc1Ga/flss2MBAFCpUQoBAADAZdSvUV+v9X1NHw/5WOsOrJMkbT++3eRUAABU\nTpRCAAAAcDlNg5oq/c50SVLXv3fVM5nPqKikyORUAABULpRCAAAAcEmGYUiStty3RbavbWr/anvu\nUgYAwM9QCgEAAMCl3VjjRq29a62Gxg5Vp9c7adZns1RcUmx2LAAATEcpBAAAAJdnGIZG3zxaX9z7\nhZbtWybrG1blfZdndiwAAExFKQQAAAC3EVEzQhlDMtS/cX+1faWtXvjyBZXYS8yOBQCAKSiFAAAA\n4FY8LB6a2G6iNgzdoEXbFqn7P7rr0JlDZscCAKDCUQoBAADALTWp3USfDf9MCWEJavVSK72e/brs\ndrvZsQAAqDCUQgAAAHBbnhZPPdbpMa1PXa+5X8xVn3f66NgPx8yOBQBAhaAUAgAAgNtrUaeFskZk\nKTY4VrELY7V452JmDQEAXB6lEAAAACDJ28NbT3V5SisHrdRTmU9p4HsDdfLcSbNjAQBQbiiFAAAA\ngJ+5ud7N2jpyq8JqhKn5gub6YO8HZkcCAKBcUAoBAAAA/8PH00czk2bq3dvf1UNrH1Lq0lSdunDK\n7FgAAJQpSiEAAADgN3S8saO2j9quGlVqqPmC5lqzf43ZkQAAKDOUQgAAAMDvqOpdVfN7zteivos0\ncuVI3bfiPv3w4w9mxwIA4LpRCgEAAABXIbFhonaO3qkSe4maL2iujIMZZkcCAOC6UAoBAAAAV6l6\nlep6pc8rmp88X3ctvUv3r75f5y+dNzsWAADXxNPsAAAAAKhYtnybbPk2SVJ8g3hNtU2VJFnDrLKG\nWU3L5UxujbpVO0fv1PjV4xW7IFaL+i1S+/rtzY4FAECpUAoBAAC4GcqfslHTt6beuu0tvb/7fd2W\ndpuGtBiiaQnT5OPpY3Y0AACuCqUQAAAAcB0GRA9QpwadNGrlKLV+qbXe6PeGWoW0MjsWKqGfz9Kz\n5dsc5SxFLQCzUAoBAAAA1ymoapDeH/i+3t75tpLfStaYm8foz53+LC8PL7OjoRL5efljTDNku8dm\nah4AYKNpAAAAoAwYhqHBzQcre2S2so5mqe0rbbXzxE6zYwEA8JsohQAAAIAyVK96Pa26c5XG3jxW\nXd7somc3PqvikmKzYwEA8AuUQgAAAEAZMwxDw1sO15cjvtSHeR+q4+sdlfOvHLNjAQBwGUohAAAA\noJyEBYRp/d3rNbjZYLV/tb3mbpqrEnuJ2bEAAJBEKQQAAACUK4th0bg24/T58M+1ZPcSdXmjiw6e\nOmh2LAAAKIUAAACAitCoViNl3pOpWxvdqjavtNFLW16S3W43OxYAwI1RCgEAAAAVxMPioYc7PCzb\nEJte2vKSkt9K1pHvj5gdCwDgpiiFAAAAgAoWExSjz4d/rg71O6jlwpZ6c/ubzBoCAFQ4SiEAAADA\nBF4eXnoi/gl9eNeHmvnZTPVP668TZ0+YHQsA4EYohQAAAAATxdWN0+YRmxVdO1otFrTQu7veNTsS\nAMBNUAoBAAAAJqviWUV/SfyLlt2xTI9nPK473rtD/zr/L7NjAQBcHKUQAAAAUEm0DW2r7JHZqlut\nrpovaK4V+1aYHQkA4MIohQAAAIBKxM/LT3N6zNHiAYt1/5r7NXTZUJ25eMbsWAAAF0QpBAAAAFRC\nnRt01o7RO+Tj4aNmLzbTurx1ZkcCALgYSiEAAACgkqrmXU0v9npRr/R5RcOXD9eYVWN0tvCs2bEA\nAC6CUggAAACo5JIikrRj9A6dv3ReLRa0UObXmWZHAgC4AE+zAwAAAAC4sgCfAC3qt0jL9y3XHe/d\nodua3CZJ2n1yt8IDwuXr5WtyQgCAs7liKXTx4kXFx8frxx9/VGFhofr27avp06crJSVFOTk5kqTT\np+sIrlYAACAASURBVE8rICBA2dnZys/PV5MmTdS4cWNJUrt27fTCCy+U7ygAAAAAN9Hnpj7qUL+D\nZmycIUnq904/fX3ma9X2q62ImhGKCPz3PzX/+++avjVNTg0AqIyuWAr5+PgoIyNDfn5+KioqUseO\nHbVx40alpaU5nvPQQw8pICDA8XVkZKSys7PLJzEAAADg5mr51dLMpJn62+d/U874HBWXFOvw94eV\n912e8k7lKe+7PL23+z3HsYfF4/Ki6GfH9arXk8VgVwkAcEdXtXzMz89PklRYWKji4mLVrPnf/9Ng\nt9u1ZMkSZWRklE9CAAAAAL/Lw+KhsIAwhQWEKVGJlz1mt9tVcL7AURDlncpT5qFMvb7tdeWdytPp\ni6cVFhB2WVnUMLChIgIjFB4YLh9PH5NGBQAob1dVCpWUlKhly5bKy8vT6NGjFR0d7Xhsw4YNCg4O\nVkREhON7Bw8eVFxcnGrUqKGnn35aHTt2LPvkAAAAAK7IMAzVrlpbtavW1i2ht/zi8XOF53Tg1AEd\nOHVAeafytK9gn9Jz05V3Kk+HzhxSUNWgX12SFhEYoUDfQBNGBAAoK1dVClksFm3btk1nzpxR9+7d\nZbPZZLVaJUmLFy/WnXfe6XhuSEiIDh8+rMDAQG3dulX9+vXTrl275O/vf9l7Tp061XFstVod7wcA\nAACg4lT1rqpmwc3ULLjZLx4rKinS4TOHL5tltGT3Esexl8XrN/cxCvEPYVkaAJQzm80mm812za83\n7Ha7vTQveOqpp+Tr66uHHnpIRUVFCg0N1datWxUSEvKrz09ISNCsWbPUsmXL/57UMFTK0wIAAACl\nZkwzZJ/iutedZo7Pbrfr5PmTl+1jlHfqv8dnfjyj8IDwXy2NwgLCVMWzyu++Pz87ACi90vYtV5wp\nVFBQIE9PTwUEBOjChQtat26dpkyZIklav369mjRpclkhVFBQoMDAQHl4eOjAgQPKzc1Vw4YNr2Eo\nAAAAACorwzAUVDVIQVWD1K5+u188frbw7E9L0v5dFu0+uVsrclYo71Sejnx/RHWq1flFWdQwsKEi\nakYowCfgV84IAChrVyyFjh07piFDhqikpEQlJSVKTU1VYuJPm9elpaVp0KBBlz0/MzNTkydPlpeX\nlywWixYuXHjZnckAAAAAuL5q3tXUPLi5mgc3/8Vjl4ov6dCZQ8o7lecojrKOZjlmGf1nFtFTnzyl\nno16Kq5uHEvRAKAclHr5WJmclOVjAAAAqACuvkTHFcdnt9v17blvVWdWHT1wywNKz03X6Yunldwo\nWcmRyUqKSHKJmUSu+LMDYL7S9i2UQgAAAHAptnybbPk2x7E1zCpJsoZZHceuwpWLhZ+P7cCpA1qd\nu1rp+9OV+XWm4urEqWejnurZqKeaBTWTYRgmpy09V/zZudPfPaCyohQCAAAA3IQrFgv/8Vtju3Dp\ngmz5NqXnpit9f7p+LPpRyZHJ6tmop7o27Cr/Kv6/8m6Vjyv/7CTXHx9QWZX5RtMAAAAAUFn4evn+\ntJSsUbLm2ecp97tcpeem68XNL+ruD+5Wm3ptHCVRkxuaOOUsIgCoKJRCAAAAAJySYRiKqhWlqFpR\nmnDLBJ0tPKuMgxlKz01X8lvJMmQ4lpklhCWoqndVsyMDQKVCKQQAAADAJVTzrqbeN/VW75t6y263\na/fJ3UrPTdfsz2dr0PuD1L5+e/WM/KkkalSrkdlxAcB0lEIAAAAAXI5hGIoJilFMUIwe7vCwvv/x\ne60/sF7puel69tNn5efl55hFFN8gXr5evmZHBoAKRykEAAAAwOVVr1JdtzW5Tbc1uU12u107TuxQ\nem66ntnwjAa+O1CdG3R27EUUHhhudlwAqBCUQgAAAADcimEYalGnhVrUaaFJnSbp1IVTWpu3Vqv3\nr9aTmU+qpm9NxzKzjjd2VBXPKmZHBoByQSkEAAAAwK0F+gYqpWmKUpqmqMReoq3Htio9N11//vjP\n2lOwRwlhCerZqKeSI5NVv0Z9s+MCQJmhFAIAAACAf7MYFrUOaa3WIa01OX6yTp47qbV5a5W+P12P\nffSYQvxDHMvM2tdvLy8PL7MjA8A1oxQCAAAAgN9Qu2ptDW4+WIObD1ZxSbGyjmZp9f7VenDtg8o7\nlaeuDbuqZ2RP9Yjsobr+dc2OCwClQikEAAAAAFfBw+KhdvXbqV39dnoy4UkdP3tca/avUXpuuiau\nnajwgHDHHc3a1msrD4uH2ZEB4HdRCgEAAADANahTrY7uib1H98Teo0vFl7TpyCal56Zr9KrROvL9\nESVFJDlmEdWuWtvsuADwC5RCAAAAAHCdvDy81KlBJ3Vq0EnTu07Xke+PaM3+NVq6d6nGrR6nxjc0\nVs/InkpulKzWIa3NjgsAkiiFAAAAAKDMhVYP1b0t79W9Le9VYXGhNh7aqNW5qzV02VCdPHdSkvTu\nrneVFJGkGj41TE4LwF1ZzA4AAAAAAK7M28NbXcK7aGbSTO0as0tZI7IkSa9ve12hc0KV+Gai5nw+\nR7n/yjU5KQB3w0whAAAAAKhAYQFhkqT0wek6V3hOHx38SCv2rdBfP/urqleprl6Neqn3Tb3VoX4H\nbnkPoFxRCgEAAACASap6V1Wfm/qoz019VGIvUfaxbK3MWamH1z2svO/ylBSRpF5RvZQcmaxafrXM\njgvAxVAKAQAAAEAlYDEsahXSSq1CWmmKdYqO/XBM6bnpem/3exqzaoyaBzdX76je6hXVS9G1o2UY\nhtmRATg5SiEAAAAAqITq+tfV8JbDNbzlcF0suihbvk0rc1aq59s9ZTEs6tWol3pF9ZI1zKoqnlXM\njgvACbHRNAAAAABUcj6ePuoR2UPze85X/v35WjFohUL8Q/Rk5pMK+luQbku7Ta9lv6bjZ4+bHRWA\nE2GmEAAAAAA4EcMw1DSoqZoGNdWkTpNUcL5Aq3NXa2XuSj249kE1qtlIvaJ+mkUUVyeOZWYAfhOl\nEAAAAAA4sRv8blBqi1SltkjVpeJL2nhoo1bkrFDKeyk6f+m8Y5lZYsNE+Xn5mR0XQCXC8jEAAAAA\ncBFeHl5KCE/Q7O6zlTs+VxlDMnTTDTdpzqY5qvO3Orr17Vv14pcv6tCZQ2ZHBVAJMFMIAAAAAFxU\nVK0oTWw3URPbTdTpi6e1Nm+tVuas1BMZT6he9XqOu5ndHHKzPCweZscFUMEohQAAAADADQT4BGhg\nzEANjBmo4pJibTqySStzVmrEihE6cfaEejbqqV5RvZQUkaTqVaqbHRdABaAUAgAAAAA342HxUIcb\nO6jDjR00vet05Z/O16qcVXpl6ysaumyo2tZr65hFFFEzwuy4AMoJpRAAAAAAuLmwgDCNbTNWY9uM\n1dnCs1p/YL1W5qzUjE9nKMAnwLFZdYcbO8jTwq+RgKvgbzMAAAAAwKGadzX1a9xP/Rr3U4m9RFuP\nbdXKnJWauHaiDp46qB6RPdQrqpd6RPZQTd+aZscFcB0ohQAAAAAAv8piWNQ6pLVah7TWVOtUHf3+\nqNJz05W2K02jVo5SbJ1Y9Yr6aRZRkxuayDAMsyMDKAVKIQAAAADAValXvZ5GtBqhEa1G6MKlC7Ll\n27QyZ6V6/KOHPC2e6hXVS72jepsdE8BVohQCAAAAAJSar5evkhslK7lRsub3nK+d3+7UypyVmmyb\nLEkatmyYUmJS1CW8i7w8vExOe/1s+TbZ8m2OY2uYVZJkDbM6jgFnY9jtdnuFn9QwZMJpAQAAAJdi\nTDNkn+Ka19WuPDbJPcY3K2mW0nal6cCpAxrQZIBSYlLUuUFneVg8zI533Vz95wfnVdq+xVKOWQAA\nAAAAbmpiu4n64t4vlHVvlsIDwvXg2gcVOidU49PH69NDn6rEXmJ2RMDtUQoBAAAAAMpNeGC4Hun4\niLaO3KpP7vlEQVWDNHLlSDX4vwZ68MMHlXU0i5UkgEkohQAAAAAAFSKqVpSeiH9CX435SmsGr1FV\n76pKXZqqiHkRenT9o8o+lk1BBFQgSiEAAAAAQIWLCYrRkwlPau/Yvfpnyj8lSbctuU2Nn2+syRmT\ntevbXSYnBFwfdx8DAAAAAJjGMAzF1olVbJ1YTU+cri+/+VJpX6Wpx1s9VKNKDaXEpCilaYqiakWZ\nHRVwOZRCAAAAAIBKwTAMtanXRm3qtdHMpJn67PBnSvsqTfGL4lW3Wl2lxKRoYMxAhQeGmx0VcAmU\nQgAAAACASsdiWNTxxo7qeGNH/V+P/1Pm15lK25WmNq+0UcPAho6CKLR6qNlRAafFnkIAAAAAgErN\nw+KhhPAELei1QN9M/EZPJTylr779Si0WtFCn1ztpftZ8HT973OyYgNNhphAAAAAAwGl4eXgpKSJJ\nSRFJKiwu1Nq8tUrblaYnMp5QXJ04pcSkaED0AN3gd4PZUYFKj1IIAAAAAOCUvD281Suql3pF9dKF\nSxe0Zv8avbPrHf1p/Z/ULrSdUmJS1K9xPwX6BpodFaiUKIUAAAAAAE7P18tX/Zv0V/8m/XWu8JxW\n5qxU2q40Tfhwgjo36KyUmBT1vamv/Kv4mx0VqDQohQAAAAAnYsu3yZZvkyTFN4jXVNtUSZI1zCpr\nmNW0XEBlUtW7qlKa/nQr++9//F7L9i7TO1+9o7HpY9W1YVelxKTo1ka3qqp3VbOjAqaiFAIAAACc\nCOUPUDrVq1RXaotUpbZI1akLp7R071K9mv2q7ltxn3pE9lBKTIqSGyXLx9PH7Kim+nnhbMu3OT5n\n+MxxbYbdbrdX+EkNQyacFgAAAICTMKYZsk9x3d8ZGJ/5Tp47qX/u+afe2fWOth3fpt5RvZUSk6Ju\nEd3k7eH9u691hvFdD1cfnysrbd/CLekBAAAAAG6ndtXaGtl6pDKGZGj3mN26OeRmTd84XXVn1dXw\nZcO1Nm+tikqKzI4JlCtKIQAAAACAW6vrX1fj247XxmEbtW3kNsUExejxjx9XyKwQjV45WrZ8m4pL\nis2OCZQ59hQCAAAAAODf6teor4ntJmpiu4k6cOqAluxaogc+fEAnzp7QH6L/oJSYFLMjAmWGUggA\nAABApcCd1VDZNAxsqEc7PqpHOz6qfQX7tGTXEo1YMUKSNPvz2RrSYohq+dUyOSVw7dhoGgAAAAAq\nmKtv5OvK47Pb7bI8adFdze/Sin0r1OemPhrZaqTa128vwzDMjlcmXPnn5+rYaBoAAAAAgHLyn+Ln\n7/3/rrw/5im2TqyGLhuq5gua6/ms53Xm4hmTEwJXj1IIAAAAAIBrUMuvlia2m6h94/Zpbo+5yjyU\nqbC5Ybp3+b3a/M1ms+MBV0QpBAAAAADAdTAMQ13CuyjtD2naM3aPIgIjdPu7t6v1S6318paXdbbw\nrNkRgV9FKQQAAAAAQBmpU62OJnWapLw/5unpLk9rVe4q3TjnRo1dNVY7TuwwOx5wGUohAAAAAADK\nmMWwqEdkD31wxwfaMXqHgqoGqedbPdX+1fZ6c/ubunDpgtkRAUohAAAAAADKU2j1UE2xTlH+hHw9\n0uERvfPVO6o/p74eWPOA9hbsNTse3Jin2QEAAAAAwB3Y8m2y5dskSfEN4jXVNlWSZA2zyhpmNS0X\nKo6nxVN9G/dV38Z9lX86Xy9veVnWRVY1qd1EI1uNVP/G/VXFs4rZMeFGDHtpbmBfVic1DJlwWgAA\nAABABTCmGbJPcd3f+cpyfIXFhVq2d5kWblmond/u1NDYobqv1X1qGNiwTN7/Wrj6z8+VlbZvYfkY\nAAAAAAAm8fbw1u0xt2v93eu1YegGFZUUqe0rbdX9H921dM9SXSq+ZHZEuDBKIQAAAAAAKoGoWlH6\nW9LfdPiBw7q7+d2avWm2wuaGaXLGZB06c8jseHBBlEIAAAAAAFQiPp4+Gtx8sDYM3aC1d63V6Yun\nFbcwTn0W99GqnFUqLik2OyJcBHsKAQAAAACu28830rbl2xybZ7viRtpm7LlzrvCc0nalaeGWhTp+\n9rjua3mfhsUNU13/umV+LvYUcl6l7VsohQAAAAAAKAWzS5Otx7Zq4eaFWrJ7iRLDEzWq9Sh1Ce8i\ni1E2i4HMHh+uHRtNAwAAAADgwlrWbamFvRfq6wlfq2vDrnpo7UOKei5KMz+dqZPnTpodD06EUggA\nAAAAACdUvUp1jWo9Stkjs/WP2/6h3QW7FTU/Sne+f6cyv85khQ6uiFIIAAAAAAAnZhiGbgm9Ra/3\nfV0H/nhAbeu11aiVoxTzQozmfTFPpy6cMjsiKin2FAIAAAAAoBScYc8du92uDYc2aMHmBVq9f7X6\nNe6nUa1GqU29NjIM43df6wzjKw232gSdjaYBAAAAACg/zlaanDx3Uou2LdLCLQtVzbuaRrUepcHN\nBsu/iv+vPt/Zxlcarjw2qYxLoYsXLyo+Pl4//vijCgsL1bdvX02fPl0pKSnKycmRJJ0+fVoBAQHK\nzs6WJE2fPl2vvfaaPDw8NG/ePCUlJV13SAAAAAAAzOQKs01K7CX66MBHWrhloT46+JEGRg/UqNaj\nFFc37rLnuXJx4spjk0rft3j+3oM+Pj7KyMiQn5+fioqK1LFjR23cuFFpaWmO5zz00EMKCAiQJO3e\nvVtpaWnavXu3jh49qq5duyonJ0cWC1sXAQAAAACclzOVP7/FYljULaKbukV00zc/fKPXsl9Tv7R+\nqlOtjka1GqWUpiny8/IzOyYq0BXbGj+/n/5AFBYWqri4WDVr1nQ8ZrfbtWTJEg0aNEiStGzZMg0a\nNEheXl4KCwtTZGSksrKyyik6AAAAAAC4FiH+IXq88+M68McDmtx5sv6595+qP6e+7l99v9nRUIGu\nWAqVlJQoNjZWwcHBSkhIUHR0tOOxDRs2KDg4WBEREZKkb775RqGhoY7HQ0NDdfTo0XKIDQAAAAAA\nrpeHxUO3Rt2qFYNWKHtktrw9vCVJT3z8hL7/8XuT06G8XbEUslgs2rZtm44cOaLMzEzZbDbHY4sX\nL9add975u6+/0q7mAAAAAADAfDfWuFEzk2ZKkg5/f1hRz0VpftZ8FRYXmpwM5eV39xT6uRo1aujW\nW2/V5s2bZbVaVVRUpKVLl2rr1q2O59SrV0+HDx92fH3kyBHVq1fvV99v6tSpjmOr1Sqr1Vr69AAA\nAAAAoMwt6rdI249v1yPrH9HcL+ZqeuJ0DWgygIkflYzNZrts8k5p/e7dxwoKCuTp6amAgABduHBB\n3bt315QpU5SYmKg1a9bo2WefVUZGhuP5u3fv1p133qmsrCzHRtP79+//xR8a7j4GAAAAAEDl9L93\n6FqXt05/Wv8n+Xj66K9d/6pODTqZmO76cPexy/3uTKFjx45pyJAhKikpUUlJiVJTU5WYmChJSktL\nc2ww/R/R0dEaOHCgoqOj5enpqRdeeIEWEQAAAAAAJ9Ytopu2NNyit3e+rbuW3qW4OnGa0XWGGt/Q\n2OxouE6/O1Oo3E7KTCEAAAAAACql35tNc7Hoop7Pel4zPp2hAU0GaEr8FNX1r1vBCa8dM4Uud8WN\npgEAAAAAACTJx9NHD7Z/UPvG7ZO/t7+avthUUzKm6IcffzA7Gq4BpRAAAAAAACiVmr41NTNpprbc\nt0UHTh9Q1Pwovfjli7pUfMnsaCgFSiEAAAAAAHBNwgLC9Pf+f9eqO1fp/T3vq+mLTbV0z1K2jHES\nlEIAAAAAAOC6tKzbUutS12lej3ma+slUdXy9oz47/JnZsXAFlEIAAAAAAOC6GYah7pHdtfW+rbqv\n5X264707NGDJAOX8K8fsaPgNlEIAAAAAAKDMeFg8NCR2iPaN26c2IW3U4bUOGrNqjE6cPWF2NPwP\nSiEAAAAAAFDmfL189UjHR7R37F75ePoo+oVoPfnJkzpbeNbsaPg3SiEAAAAAAFBuavnV0uzus/Xl\niC+1t2Cvop6L0ktbXlJRSZHZ0dwepRAAAAAAACh3DQMb6u0Bb2v5oOVa/NViNXuxmZbvW86dykxE\nKQQAAAAAACpM65DW+vjujzUraZYe++gxxS+K1xdHvjA7lluiFAIAAAAAABXKMAz1bNRT20dt19DY\noRqwZIBuf/d25f4r1+xoboVSCAAAAAAAmMLD4qGhcUOVMz5HLeu0VLtX22l8+nh9e+5bs6O5BUoh\nAAAAAABgKj8vP03qNEl7xu6RxbAo+vloPZP5jM5fOm92NJdGKQQAAAAAACqF2lVra27yXG26d5N2\nfLtDUc9F6dWtr3KnsnJCKQQAAAAAACqVyJqRSvtDmt4f+L7e3PGmYhfEalXOKu5UVsYohQAAAAAA\nQKXUNrStbENsmp44XQ+ve1gJbyToy6Nfmh3LZVAKAQAAAACASsswDPW+qbd2jN6hwc0Gq19aP93x\n3h3K+y7P7GhOj1IIAAAAAABUep4WT41oNUI543LUNKip2rzSRhPWTFDB+QKzozktSiEAAAAAAOA0\nqnpX1eOdH9eesXtUVFKkxvMba8bGGbpw6YLZ0ZwOpRAAAAAAAHA6QVWDNL/nfH02/DNt/mazouZH\n6fXs11VcUmx2NKdBKQQAAAAAAJxWVK0ovTfwPS35wxK9mv2qYhfGanXuau5UdhUohQAAAAAAgNNr\nV7+dNgzdoKcTntYDHz6grn/vqi3fbDE7VqVm2E2ozgzDoLEDAAAAAKCSsOXbZMu3OY6tYVZJkjXM\n6jh2JkUlRXp166ua+slUdQnvoqcTnlZ4YLiMaYbsU1y3jyht30IpBAAAAAAAXNLZwrOa9dkszcua\np3ta3KPZm2ZTCv0My8cAAAAAAIBLquZdTVOsU7RrzC6dv3Rekpik8jPMFAIAAAAAAG6B5WOXY6YQ\nAAAAAACAG6IUAgAAAAAAcEOUQgAAAAAAAG6IUggAAAAAAMANUQoBAAAAAAC4IUohAAAAAAAAN0Qp\nBAAAAAAA4IYohQAAAAAAANwQpRAAAAAAAIAbohQCAAAAAABwQ5RCAAAAAAAAbohSCAAAAAAAwA1R\nCgEAAAAAALghSiEAAAAAAAA3RCkEAAAAAADghiiFAAAAAAAA3BClEAAAAAAAgBuiFAIAAAAAAHBD\nlEIAAAAAAABuiFIIAAAAAADADVEKAQAAAAAAuCFKIQAAAAAAADdEKQQAAAAAAOCGKIUAAAAAAADc\nEKUQAAAAAACAG6IUAgAAAAAAcEOUQgAAAAAAAG6IUggAAAAAAMANUQoBAAAAAAC4IUohAAAAAAAA\nN0QpBAAAAAAA4IYohQAAAAAAANwQpRAAAAAAAIAbohQCAAAAAABwQ4bdbrdX+EkNQyacFgAAAAAA\nuBlbvk22fJvj2BpmlSRZw6yOY1dR2r6FUggAAAAAAMAFlLZvYfkYAAAAAACAG6IUAgAAAAAAcEOU\nQgAAAAAAAG6IUggAAAAAAMANUQoBAAAAAAC4IUohAAAAAAAAN0QpBAAAAAAA4IYohQAAAAAAANwQ\npRAAAAAAAIAbumIpdPHiRbVt21axsbGKjo7WpEmTHI8999xzatKkiZo2bapHHnlEkpSfny9fX1/F\nxcUpLi5OY8aMKb/0AAAAAAAAuCaeV3qCj4+PMjIy5Ofnp6KiInXs2FEbN27UpUuXtHz5cu3YsUNe\nXl46efKk4zWRkZHKzs4u1+AA8L9sNpusVqvZMQC4GD5bAJQHPlsAVAZXtXzMz89PklRYWKji4mIF\nBgZqwYIFmjRpkry8vCRJtWvXLr+UAHAVbDab2REAuCA+WwCUBz5bAFQGV1UKlZSUKDY2VsHBwUpI\nSFBMTIxycnKUmZmpW265RVarVZs3b3Y8/+DBg4qLi5PVatXGjRvLLTwAAAAAAACuzRWXj0mSxWLR\ntm3bdObMGXXv3l02m01FRUU6deqUNm3apC+//FIDBw7UgQMHFBISosOHDyswMFBbt25Vv379tGvX\nLvn7+5f3WAAAAAAAAHCVDLvdbi/NC5566in5+vrqo48+0qOPPqr4+HhJP+0j9MUXX6hWrVqXPT8h\nIUGzZs1Sy5YtHd+LjIxUXl5eGcQHAAAAAACAJEVERGj//v1X/fwrzhQqKCiQp6enAgICdOHCBa1b\nt05TpkyRv7+/Pv74Y8XHxysnJ0eFhYWqVauWCgoKFBgYKA8PDx04cEC5ublq2LDhZe9ZmoAAAAAA\nAAAoe1cshY4dO6YhQ4aopKREJSUlSk1NVWJiojp37qxhw4apWbNm8vb21ptvvilJyszM1OTJk+Xl\n5SWLxaKFCxcqICCg3AcCAAAAAACAq1fq5WMAAAAAAABwfld197GytGbNGjVu3FiNGjXSs88+W9Gn\nB+CiwsLC1Lx5c8XFxalNmzZmxwHgpIYNG6bg4GA1a9bM8b3vvvtO3bp1U1RUlJKSknT69GkTEwJw\nRr/22TJ16lSFhoYqLi5OcXFxWrNmjYkJATijw4cPO+4Q37RpU82bN09S6a5dKrQUKi4u1rhx47Rm\nzRrt3r1bixcv1p49eyoyAgAXZRiGbDabsrOzlZWVZXYcAE5q6NChv/jFbMaMGerWrZtycnKUmJio\nGTNmmJQOgLP6tc8WwzA0ceJEZWdnKzs7Wz169DApHQBn5eXlpTlz5mjXrl3atGmTnn/+ee3Zs6dU\n1y4VWgplZWUpMjJSYWFh8vLy0h133KFly5ZVZAQALozVsACuV6dOnRQYGHjZ95YvX64hQ4ZIkoYM\nGaIPPvjAjGgAnNivfbZIXLsAuD516tRRbGysJKlatWpq0qSJjh49WqprlwothY4ePar69es7vg4N\nDdXRo0crMgIAF2UYhrp27arWrVvr5ZdfNjsOABdy4sQJBQcHS5KCg4N14sQJkxMBcBXPPfecWrRo\noeHDh7M0FcB1yc/PV3Z2ttq2bVuqa5cKLYUMw6jI0wFwI59++qmys7O1evVqPf/889qwYYPZkQC4\nIMMwuJ4BUCZGjx6tgwcPatu2bapbt64efPBBsyMBcFJnz57VgAEDNHfuXPn7+1/22JWuXSq0FKpX\nr54OHz7s+Prw4cMKDQ2tyAgAXFTdunUlSbVr11b//v3ZVwhAmQkODtbx48clSceOHVNQUJDJWjYy\nvwAAAYNJREFUiQC4gqCgIMcva/feey/XLgCuyaVLlzRgwAClpqaqX79+kkp37VKhpVDr1q2Vm5ur\n/Px8FRYWKi0tTX369KnICABc0Pnz5/XDDz9Iks6dO6e1a9dedncPALgeffr00RtvvCFJeuONNxwX\nXABwPY4dO+Y4Xrp0KdcuAErNbrdr+PDhio6O1oQJExzfL821i2Gv4N3NVq9erQkTJqi4uFjDhw/X\npEmTKvL0AFzQwYMH1b9/f0lSUVGRBg8ezGcLgGsyaNAgffLJJyooKFBwcLCefPJJ9e3bVwMHDtSh\nQ4cUFhamJUuWKCAgwOyoAJzI/362TJs2TTabTdu2bZNhGAoPD9fChQsde4AAwNXYuHGjOnfurObN\nmzuWiE2fPl1t2rS56muXCi+FAAAAAAAAYL4KXT4GAAAAAACAyoFSCAAAAAAAwA1RCgEAAAAAALgh\nSiEAAAAAAAA3RCkEAAAAAADghiiFAAAAAAAA3BClEAAAAAAAgBuiFAIAAAAAAHBD/w8hfJc1eBoH\n4gAAAABJRU5ErkJggg==\n",
      "text/plain": [
       "<matplotlib.figure.Figure object at 0x7f8722ad42b0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "inertia_means = np.mean(inertia_scores, axis=1)\n",
    "inertia_stderr = np.std(inertia_scores, axis=1)\n",
    "\n",
    "fig = plt.figure(figsize=(20,10))\n",
    "plt.errorbar(n_cluster_values, inertia_means, inertia_stderr, color='green')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Pipeline(steps=[('feature_extraction', TfidfVectorizer(analyzer='word', binary=False, charset=None,\n",
       "        charset_error=None, decode_error='strict',\n",
       "        dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',\n",
       "        lowercase=True, max_df=0.4, max_features=None, min_df=1,\n",
       "        ngram_range=(... n_init=10,\n",
       "    n_jobs=1, precompute_distances=True, random_state=None, tol=0.0001,\n",
       "    verbose=0))])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "n_clusters = 6\n",
    "\n",
    "pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),\n",
    "                     ('clusterer', KMeans(n_clusters=n_clusters))\n",
    "                     ])\n",
    "pipeline.fit(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "labels = pipeline.predict(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cluster 0 contains 21 samples\n",
      "  Most important terms\n",
      "  1) korea (score: 0.1591)\n",
      "  2) korean (score: 0.1573)\n",
      "  3) south (score: 0.1336)\n",
      "  4) kim (score: 0.1088)\n",
      "  5) north (score: 0.1077)\n",
      "\n",
      "Cluster 1 contains 33 samples\n",
      "  Most important terms\n",
      "  1) iran (score: 0.1047)\n",
      "  2) netanyahu (score: 0.0869)\n",
      "  3) nuclear (score: 0.0660)\n",
      "  4) he (score: 0.0615)\n",
      "  5) his (score: 0.0550)\n",
      "\n",
      "Cluster 2 contains 156 samples\n",
      "  Most important terms\n",
      "  1) palestinian (score: 0.0196)\n",
      "  2) israel (score: 0.0138)\n",
      "  3) security (score: 0.0118)\n",
      "  4) bank (score: 0.0115)\n",
      "  5) its (score: 0.0103)\n",
      "\n",
      "Cluster 3 contains 31 samples\n",
      "  Most important terms\n",
      "  1) browser (score: 0.2786)\n",
      "  2) able (score: 0.2429)\n",
      "  3) you (score: 0.2018)\n",
      "  4) css (score: 0.1925)\n",
      "  5) sheets (score: 0.1925)\n",
      "\n",
      "Cluster 4 contains 48 samples\n",
      "  Most important terms\n",
      "  1) al (score: 0.0862)\n",
      "  2) isis (score: 0.0723)\n",
      "  3) islamic (score: 0.0664)\n",
      "  4) iraq (score: 0.0657)\n",
      "  5) state (score: 0.0580)\n",
      "\n",
      "Cluster 5 contains 201 samples\n",
      "  Most important terms\n",
      "  1) he (score: 0.0448)\n",
      "  2) we (score: 0.0306)\n",
      "  3) they (score: 0.0294)\n",
      "  4) his (score: 0.0254)\n",
      "  5) who (score: 0.0253)\n",
      "\n"
     ]
    }
   ],
   "source": [
    "c = Counter(labels)\n",
    "\n",
    "terms = pipeline.named_steps['feature_extraction'].get_feature_names()\n",
    "\n",
    "for cluster_number in range(n_clusters):\n",
    "    print(\"Cluster {} contains {} samples\".format(cluster_number, c[cluster_number]))\n",
    "    print(\"  Most important terms\")\n",
    "    centroid = pipeline.named_steps['clusterer'].cluster_centers_[cluster_number]\n",
    "    most_important = centroid.argsort()\n",
    "    for i in range(5):\n",
    "        term_index = most_important[-(i+1)]\n",
    "        print(\"  {0}) {1} (score: {2:.4f})\".format(i+1, terms[term_index], centroid[term_index]))\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.031820420989154712"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.metrics import silhouette_score\n",
    "X = pipeline.named_steps['feature_extraction'].transform(documents)\n",
    "silhouette_score(X, labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "14327"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(terms)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "Y = pipeline.transform(documents) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "km = KMeans(n_clusters=n_clusters)\n",
    "labels = km.fit_predict(Y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cluster 0 contains 89 samples\n",
      "Cluster 1 contains 65 samples\n",
      "Cluster 2 contains 7 samples\n",
      "Cluster 3 contains 24 samples\n",
      "Cluster 4 contains 290 samples\n",
      "Cluster 5 contains 15 samples\n"
     ]
    }
   ],
   "source": [
    "c = Counter(labels)\n",
    "for cluster_number in range(n_clusters):\n",
    "    print(\"Cluster {} contains {} samples\".format(cluster_number, c[cluster_number]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.56564461613141714"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "silhouette_score(Y, labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(490, 6)"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Evidence Accumulation Clustering"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from scipy.sparse import csr_matrix\n",
    "\n",
    "\n",
    "def create_coassociation_matrix(labels):\n",
    "    rows = []\n",
    "    cols = []\n",
    "    unique_labels = set(labels)\n",
    "    for label in unique_labels:\n",
    "        indices = np.where(labels == label)[0]\n",
    "        for index1 in indices:\n",
    "            for index2 in indices:\n",
    "                rows.append(index1)\n",
    "                cols.append(index2)\n",
    "    data = np.ones((len(rows),))\n",
    "    return csr_matrix((data, (rows, cols)), dtype='float')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "C = create_coassociation_matrix(labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<490x490 sparse matrix of type '<class 'numpy.float64'>'\n",
       "\twith 97096 stored elements in Compressed Sparse Row format>"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "C"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((490, 490), 240100)"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "C.shape, C.shape[0] * C.shape[1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.4043981674302374"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(C.nonzero()[0]) / (C.shape[0] * C.shape[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from scipy.sparse.csgraph import minimum_spanning_tree"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "mst = minimum_spanning_tree(C)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<490x490 sparse matrix of type '<class 'numpy.float64'>'\n",
       "\twith 484 stored elements in Compressed Sparse Row format>"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mst"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),\n",
    "                     ('clusterer', KMeans(n_clusters=3))\n",
    "                     ])\n",
    "pipeline.fit(documents)\n",
    "labels2 = pipeline.predict(documents)\n",
    "C2 = create_coassociation_matrix(labels2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "matrix([[ 1. ,  0.5,  0.5, ...,  0.5,  0.5,  0.5],\n",
       "        [ 0.5,  1. ,  1. , ...,  1. ,  1. ,  1. ],\n",
       "        [ 0.5,  1. ,  1. , ...,  1. ,  1. ,  1. ],\n",
       "        ..., \n",
       "        [ 0.5,  1. ,  1. , ...,  1. ,  1. ,  1. ],\n",
       "        [ 0.5,  1. ,  1. , ...,  1. ,  1. ,  1. ],\n",
       "        [ 0.5,  1. ,  1. , ...,  1. ,  1. ,  1. ]])"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "C_sum = (C + C2) / 2\n",
    "#C_sum.data = C_sum.data\n",
    "C_sum.todense()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<490x490 sparse matrix of type '<class 'numpy.float64'>'\n",
       "\twith 489 stored elements in Compressed Sparse Row format>"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "mst = minimum_spanning_tree(-C_sum)\n",
    "mst"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<490x490 sparse matrix of type '<class 'numpy.float64'>'\n",
       "\twith 481 stored elements in Compressed Sparse Row format>"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#mst.data[mst.data < 1] = 0\n",
    "mst.data[mst.data > -1] = 0\n",
    "mst.eliminate_zeros()\n",
    "mst"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from scipy.sparse.csgraph import connected_components\n",
    "number_of_clusters, labels = connected_components(mst)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "\n",
    "from sklearn.base import BaseEstimator, ClusterMixin\n",
    "\n",
    "class EAC(BaseEstimator, ClusterMixin):\n",
    "    def __init__(self, n_clusterings=10, cut_threshold=0.5, n_clusters_range=(3, 10)):\n",
    "        self.n_clusterings = n_clusterings\n",
    "        self.cut_threshold = cut_threshold\n",
    "        self.n_clusters_range = n_clusters_range\n",
    "    \n",
    "    def fit(self, X, y=None):\n",
    "        C = sum((create_coassociation_matrix(self._single_clustering(X))\n",
    "                 for i in range(self.n_clusterings)))\n",
    "        mst = minimum_spanning_tree(-C)\n",
    "        mst.data[mst.data > -self.cut_threshold] = 0\n",
    "        mst.eliminate_zeros()\n",
    "        self.n_components, self.labels_ = connected_components(mst)\n",
    "        return self\n",
    "    \n",
    "    def _single_clustering(self, X):\n",
    "        n_clusters = np.random.randint(*self.n_clusters_range)\n",
    "        km = KMeans(n_clusters=n_clusters)\n",
    "        return km.fit_predict(X)\n",
    "    \n",
    "    def fit_predict(self, X):\n",
    "        self.fit(X)\n",
    "        return self.labels_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)),\n",
    "                     ('clusterer', EAC())\n",
    "                     ])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Pipeline(steps=[('feature_extraction', TfidfVectorizer(analyzer='word', binary=False, charset=None,\n",
       "        charset_error=None, decode_error='strict',\n",
       "        dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',\n",
       "        lowercase=True, max_df=0.4, max_features=None, min_df=1,\n",
       "        ngram_range=(...ocabulary=None)), ('clusterer', EAC(cut_threshold=0.5, n_clusterings=10, n_clusters_range=(3, 10)))])"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pipeline.fit(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "labels = pipeline.named_steps['clusterer'].labels_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "c = Counter(labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Counter({0: 490})"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "c"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Online Learning"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from sklearn.cluster import MiniBatchKMeans"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "vec = TfidfVectorizer(max_df=0.4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "X = vec.fit_transform(documents)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "mbkm = MiniBatchKMeans(random_state=14, n_clusters=3)\n",
    "batch_size = 500\n",
    "\n",
    "indices = np.arange(0, X.shape[0])\n",
    "for iteration in range(100):\n",
    "    sample = np.random.choice(indices, size=batch_size, replace=True)\n",
    "    mbkm.partial_fit(X[sample[:batch_size]])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "mbkm = MiniBatchKMeans(random_state=14, n_clusters=3)\n",
    "batch_size = 10\n",
    "\n",
    "for iteration in range(int(X.shape[0] / batch_size)):\n",
    "    start = batch_size * iteration\n",
    "    end = batch_size * (iteration + 1)\n",
    "    mbkm.partial_fit(X[start:end])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "401.6913550000382"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "labels_mbkm = mbkm.predict(X)\n",
    "mbkm.inertia_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "392.6561949236311"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "km = KMeans(random_state=14, n_clusters=3)\n",
    "labels_km = km.fit_predict(X)\n",
    "km.inertia_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import adjusted_mutual_info_score, homogeneity_score\n",
    "from sklearn.metrics import mutual_info_score, v_measure_score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.39531764243316064"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "v_measure_score(labels_mbkm, labels_km)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(490, 14327)"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([2, 2, 0, 2, 1, 2, 2, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 1, 2, 0, 2, 2, 2, 1, 2, 2, 2, 2, 2,\n",
       "       2, 2, 1, 1, 1, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0,\n",
       "       2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 1, 0, 0, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 2, 1, 0, 0, 2, 2, 1,\n",
       "       2, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0,\n",
       "       0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 0, 0, 2, 0, 2, 2, 0, 2,\n",
       "       2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 0, 2, 2,\n",
       "       2, 1, 2, 0, 2, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 0,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 0, 2, 0, 2, 0, 2, 2,\n",
       "       2, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 0, 1, 2, 2, 2, 2, 2, 2,\n",
       "       0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 2,\n",
       "       2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 1, 0, 0, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2,\n",
       "       2, 2, 0, 2, 1, 2, 2, 2, 2, 1, 2, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2,\n",
       "       0, 2, 0, 2, 2, 0, 2, 2, 2, 1, 2, 2, 0, 2, 2, 1, 2, 2, 0, 0, 2, 2, 0,\n",
       "       2, 2, 2, 1, 2, 1, 2, 1, 2, 2, 0, 0, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0,\n",
       "       0, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 2, 2, 0, 0, 1, 2, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 2, 0, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 2, 2, 0, 0, 1, 2, 2, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 0, 2,\n",
       "       2, 0, 0, 0, 2, 2, 0], dtype=int32)"
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "labels_mbkm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from sklearn.feature_extraction.text import HashingVectorizer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "class PartialFitPipeline(Pipeline):\n",
    "    def partial_fit(self, X, y=None):\n",
    "        Xt = X\n",
    "        for name, transform in self.steps[:-1]:\n",
    "            Xt = transform.transform(Xt)\n",
    "        return self.steps[-1][1].partial_fit(Xt, y=y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "pipeline = PartialFitPipeline([('feature_extraction', HashingVectorizer()),\n",
    "                             ('clusterer', MiniBatchKMeans(random_state=14, n_clusters=3))\n",
    "                             ])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "batch_size = 10\n",
    "\n",
    "for iteration in range(int(len(documents) / batch_size)):\n",
    "    start = batch_size * iteration\n",
    "    end = batch_size * (iteration + 1)\n",
    "    pipeline.partial_fit(documents[start:end])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 2, 2, 0, 1, 0, 2, 2, 2, 2, 1, 2, 2, 0, 2, 2, 0, 2, 0, 2, 2, 1, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 0, 2, 1, 2, 2, 2, 1, 2,\n",
       "       2, 2, 2, 1, 1, 1, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 1, 0, 0, 2,\n",
       "       1, 2, 2, 2, 1, 0, 0, 2, 2, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 0, 2, 2, 0,\n",
       "       0, 2, 0, 2, 2, 0, 0, 2, 2, 2, 1, 0, 0, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1,\n",
       "       2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 0, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 2, 0, 1, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 2, 2, 2, 0, 0, 0, 1, 0, 2,\n",
       "       1, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 0, 2, 0, 2, 2, 2,\n",
       "       0, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 2, 2, 0,\n",
       "       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 0, 2, 2, 2, 2,\n",
       "       2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       2, 1, 2, 0, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2,\n",
       "       0, 2, 2, 1, 0, 1, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,\n",
       "       2, 2, 2, 1, 2, 2, 2, 0, 0, 0, 2, 0, 2, 2, 0, 0, 2, 1, 2, 2, 2, 2, 2,\n",
       "       0, 2, 0, 2, 0, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2,\n",
       "       0, 2, 0, 2, 2, 2, 2, 1, 0, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2,\n",
       "       2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n",
       "       0, 2, 2, 2, 2, 2, 2], dtype=int32)"
      ]
     },
     "execution_count": 76,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "labels = pipeline.predict(documents)\n",
    "labels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.4.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
